/*
 * Copyright (c) 2017 上海红星美凯龙悦家互联网科技有限公司
 */

package top.tenebrous.yzl.utils;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import top.tenebrous.yzl.basic.exceptions.BizException;

import java.util.concurrent.TimeUnit;

/**
 *
 */
public class RedisLock {

    private static final Logger logger = LoggerFactory.getLogger(RedisLock.class);

    public static final String COIN_LOCK = "COIN_LOCK:";

    public static final String GROWTH_LOCK = "GROWTH_LOCK:";

    public static final String RIDER_GRAB_LOCK = "LOCK_RIDER_GRAB_ORDER:";

    private RedisTemplate redisOperations;
    private String lockKey;
    private Long localLock = null;
    private int lock = 0;
    private long lockTimeoutMillis = 60000;

    public RedisLock(RedisTemplate redisOperations, String lockKey) {
        this(redisOperations, lockKey, 60000);
    }

    public RedisLock(RedisTemplate redisOperations, String lockKey, long lockTimeoutMillis) {
        if (StringUtils.isBlank(lockKey)) {
            throw new IllegalArgumentException("lockKey cannot be null");
        }
        this.redisOperations = redisOperations;
        this.lockKey = "lock:".concat(lockKey);
        this.lockTimeoutMillis = lockTimeoutMillis;
    }

    /**
     * 获取锁,在获得锁之前一直处于阻塞
     *
     * @deprecated 一直阻塞会带来无法预知的后果, 建议使用 tryLock(long) 或 lock(long)
     */
    @Deprecated
    public void lock() {
        tryLock(-1);
    }

    public void lock(long timeoutMillis) {
        if (!tryLock(timeoutMillis)) {
            throw new BizException("lock timeout for key " + lockKey);
        }
    }

    /**
     * 尝试获取锁, 在超时时间内未获得锁将直接返回
     *
     * @param timeoutMillis 超时时间(单位:毫秒)
     * @return true:获取锁成功; false:获取锁失败
     */
    public boolean tryLock(long timeoutMillis) {
        long start = System.currentTimeMillis();
        while (true) {
            Long newTimeoutTime = System.currentTimeMillis() + lockTimeoutMillis + 1000;
            Boolean locked = redisOperations.opsForValue().setIfAbsent(lockKey, newTimeoutTime);
            if (locked) {
                logger.debug("hold lock[{}] with value [{}]", lockKey, newTimeoutTime);
                redisOperations.expire(lockKey, lockTimeoutMillis, TimeUnit.MILLISECONDS);
                this.localLock = newTimeoutTime;
                return true;
            } else {
                Long oldTimeoutTime = (Long) redisOperations.opsForValue().get(lockKey);
                if (oldTimeoutTime != null && oldTimeoutTime < System.currentTimeMillis()) {
                    //已过期
                    logger.warn("Deadlock found when trying to get lock for key [{}]", lockKey);
                    Long curTimeoutTime = (Long) redisOperations.opsForValue().getAndSet(lockKey, newTimeoutTime);
                    if (curTimeoutTime != null && oldTimeoutTime.equals(curTimeoutTime)) {
                        logger.debug("Cover lock[{}] with value [{}]", lockKey, newTimeoutTime);
                        redisOperations.expire(lockKey, lockTimeoutMillis, TimeUnit.MILLISECONDS);
                        //超时时间戳可能被其他请求重新设置,导致解锁失败,引发新一轮死锁
                        this.localLock = newTimeoutTime;
                        return true;
                    }
                }
                if (timeoutMillis > 0) {
                    long duration = System.currentTimeMillis() - start;
                    if (timeoutMillis < 50 || duration >= timeoutMillis) {
                        //获取锁超时
                        logger.warn("try to acquire lock[{}] timeout", lockKey);
                        return false;
                    }
                }
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }

    /**
     * 释放锁
     */
    public void unlock() {
        if (localLock != null) {
            Long redisLock = (Long) redisOperations.opsForValue().get(lockKey);
            if (redisLock != null && localLock.equals(redisLock)) {
                redisOperations.delete(lockKey);
                logger.debug("release lock[{}] with value[{}], elapsed {} ms", lockKey, localLock, System.currentTimeMillis() - (redisLock -
                        lockTimeoutMillis - 1000));
            } else {
                logger.warn("lock[{}] hold by other threads", lockKey);
            }
            localLock = null;
        } else {
            logger.warn("there is no lock is held, call lock() or tryLock() method first");
        }
    }


    /**
     * 是否被锁,可能是当前线程锁住,也可能是其他线程锁住
     *
     * @return
     */
    public boolean isLocked() {
        return redisOperations.hasKey(lockKey);
    }

    /**
     * 是否是被当前线程锁住的
     *
     * @return
     */
    public boolean isLocalLocked() {
        if (localLock != null) {
            Object redisLock = redisOperations.opsForValue().get(lockKey);
            return redisLock != null && redisLock.equals(localLock);
        } else {
            return false;
        }
    }

}
